namespace('js.lang');

js.lang.ClassBuilder = {

  pkg2Class: {},

  EMPTY_FUNCTION_BODY: 'function () {\n}',

  createPackage: function() {
    if (arguments.length < 2) {
      throw new js.lang.IllegalArgumentException('At least a package name and a $class should be provided.');
    }
    var argIndex = 0;
    var packageName = arguments[argIndex++];
    if (typeof(packageName) != 'string') {
      throw new js.lang.IllegalArgumentException('The provided package name should be a string.');
    }

    var importedClasses = [];
    for (; argIndex < arguments.length; argIndex++) {
      var arg = arguments[argIndex];
      if (arg.$class && arg.$class.getCanonicalClassName() == 'js.lang.Import') {
        importedClasses.push(arg.getCanonicalClass());
      } else {
        break;
      }
    }

    var classDefinition = arguments[argIndex++];
    if (classDefinition.$class.getCanonicalClassName() != 'js.lang.ClassDefinition') {
      throw new js.lang.IllegalArgumentException('The provided argument is not a $class.');
    }

    var imports = {};

    if (js.lang.ClassBuilder.pkg2Class[packageName] == undefined) {
      js.lang.ClassBuilder.pkg2Class[packageName] = [];
    } else {
      var siblingClasses = js.lang.ClassBuilder.pkg2Class[packageName];
      js.lang.ClassBuilder._addImports(siblingClasses, imports);
    }

    if (importedClasses != null) {
      js.lang.ClassBuilder._addImports(importedClasses, imports);
    }

    var constructorClass = js.lang.ClassBuilder._build(packageName, classDefinition, imports);

    var packageRef = js.lang.ClassBuilder._getPackageRef(constructorClass.$class.getPackageName());
    packageRef[constructorClass.$class.getClassName()] = constructorClass;

    js.lang.ClassBuilder.pkg2Class[packageName].push(constructorClass.$class);
  },

  createImport: function(packageToImport) {
    return new js.lang.Import(packageToImport);
  },

  createExtends: function(canonicalClassName) {
    return new js.lang.Extends(canonicalClassName);
  },

  createClassDefinition: function() {
    var obj = new Object;
    obj.__proto__ = js.lang.ClassDefinition.prototype;
    js.lang.ClassDefinition.apply(obj, arguments);
    return obj;
  },

  createAbstractClassDefinition: function() {
    var classDef = js.lang.ClassBuilder.createClassDefinition.apply(null, arguments);
    classDef._abstract = true;
    return classDef;
  },

  _addImports: function(classes, imports) {
    for (var j = 0; j < classes.length; j++) {
      var aClass = classes[j];
      imports[aClass.getClassName()] = aClass.getCanonicalClassName();
    }
  },

  _build: function(packageName, classDefinition, imports) {
    namespace(packageName);

    var superClass = classDefinition.getSuperClass() ? classDefinition.getSuperClass().getCanonicalClass(imports) : null;

    var definition = classDefinition.getClassDefinition();

    var importsAsSource = js.lang.ClassBuilder._importsToSource(imports);

    var constructorClass = js.lang.ClassBuilder._getConstructor(superClass, classDefinition, importsAsSource);    

    constructorClass.$super = superClass;

    if (superClass) {
      var prototypeClass = js.lang.ClassBuilder._createEmptyFunction();
      prototypeClass.prototype = superClass.prototype;

      // do the prototype inheritance
      constructorClass.prototype = new prototypeClass();

      constructorClass.prototype.$super = superClass;
    }

    // Set the classe's metadata
    constructorClass.$class = js.lang.ClassBuilder._createClass(packageName, classDefinition);

    constructorClass.prototype.getClass = function(){
      return constructorClass.$class
    };

    constructorClass.prototype.constructor = constructorClass;

    var staticConstructor = js.lang.ClassBuilder._processStaticBlock(classDefinition, constructorClass, importsAsSource);


    if (definition.hasOwnProperty("toString")) {
      constructorClass.prototype.toString = definition.toString;
    }
    if (definition.hasOwnProperty("valueOf")) {
      constructorClass.prototype.valueOf = definition.valueOf;
    }

    delete definition.toString;
    delete definition.valueOf;


    js.lang.ClassBuilder._inherit(constructorClass.prototype, definition, importsAsSource);

    if (!classDefinition.isAbstract()) {
      js.lang.ClassBuilder._verifyAbstractImpl(constructorClass);
    }

    if (staticConstructor) {
      staticConstructor.call(constructorClass);
    }

    return constructorClass;
  },

  _getPackageRef: function(packageName) {
    var packageRef = window;
    var packageParts = packageName.split('.');
    for (var i = 0; i < packageParts.length; i++) {
      packageRef = packageRef[packageParts[i]];
    }
    return packageRef;
  },

  _getConstructor: function(superClass, classDefinition, importsAsSource) {
    // Let's get the constructor
    var constructorName = '$' + classDefinition.getClassName();
    var definition = classDefinition.getClassDefinition();
    var constructorClass;
    if (definition[constructorName]) {
      if (importsAsSource && importsAsSource.length > 0) {
        constructorClass = js.lang.ClassBuilder._copyConstructorInClosure(importsAsSource, definition[constructorName].toString());
      } else {
        constructorClass = definition[constructorName];
      }

      delete definition[constructorName];
    } else {
      if (superClass) {
        if (importsAsSource && importsAsSource.length > 0) {
          constructorClass = js.lang.ClassBuilder._copyConstructorInClosure(importsAsSource, superClass.toString());
        } else {
          constructorClass = js.lang.ClassBuilder._copyFunction(superClass);
        }
      } else {
        constructorClass = js.lang.ClassBuilder._createEmptyFunction();
      }
    }
    return constructorClass;
  },

  _copyConstructorInClosure: function(importsAsSource, constructorAsSource) {
    var constructorClass;
    eval('(function () { ' + importsAsSource + ' constructorClass = ' + constructorAsSource + '; }) ();');
    return constructorClass;
  },

  _createClass: function(packageName, classDefinition) {
    var methods = new Array;
    var definition = classDefinition.getClassDefinition();

    if (definition.$abstract) {
      if (classDefinition._abstract) {
        if (definition.$abstract instanceof Object) {
          for (var prop in definition.$abstract) {
            var abstractMethod = definition.$abstract[prop];
            if (typeof(abstractMethod) != 'function') {
              throw new js.lang.ClassDefinitionException('Member ' + prop + ' is not a function. Only functions can be declared in the ' +
                                                         'abstract block of a class');
            } else if (abstractMethod.toString() != js.lang.ClassBuilder.EMPTY_FUNCTION_BODY) {
              throw new js.lang.ClassDefinitionException('Member ' + prop + ' cannot have an implementation as it is declared in the ' +
                                                         'abstract block of the class');
            } else {
              methods.push(new js.lang.Method(prop, true, false, false, abstractMethod.toString()));
            }
          }
          delete definition.$abstract;
        } else {
          throw new js.lang.ClassDefinitionException('The $abstract block is not an object.');
        }
      } else {
        throw new js.lang.ClassDefinitionException('This class has a $abstract block so it should be declared as $abstract_class');
      }
    }

    if (definition.$static) {
      for (var prop in definition.$static) {
        var staticMethod = definition.$static[prop];
        if (typeof(staticMethod) == 'function' && prop.indexOf('$') < 0) {
          methods.push(new js.lang.Method(prop, false, prop.indexOf('_') == 1, true, staticMethod.toString()));
        }
      }
    }

    for (var prop in definition) {
      var method = definition[prop];
      if (typeof(method) == 'function' && prop.indexOf('$') < 0) {
        methods.push(new js.lang.Method(prop, false, prop.indexOf('_') == 1, false, method.toString()));
      }
    }

    return new js.lang.Class(packageName + '.' + classDefinition.getClassName(), classDefinition._abstract, methods);
  },

  _processStaticBlock: function(classDefinition, constructorClass, importsAsSource) {
    var constructorName = '$' + classDefinition.getClassName();
    var definition = classDefinition.getClassDefinition();
    var staticConstructor;
    if (definition.$static) {
      // Get the static constructor if defined
      if (definition.$static[constructorName]) {
        staticConstructor = js.lang.ClassBuilder._copyConstructorInClosure(importsAsSource, definition.$static[constructorName].toString());        

        delete definition.$static[constructorName];
      }

      js.lang.ClassBuilder._inherit(constructorClass, definition.$static, importsAsSource);

      delete definition.$static;
    }
    return staticConstructor;
  },

  _importsToSource: function(imports) {
    var importsAsSource = '';
    if (imports) {
      for (var className in imports) {
        var classCanonicalName = imports[className];
        importsAsSource += ('var ' + className + ' = ' + classCanonicalName + '; ');
      }
    }
    return importsAsSource;
  },

  _inherit: function(destination, source, importsAsSource) {
    for (var name in source) {
      if (importsAsSource && importsAsSource.length > 0 && source[name] && typeof(source[name]) === 'function') {
        eval('(function () { ' + importsAsSource + ' destination[name] = ' + source[name].toString() + '; }) ();');
      } else {
        destination[name] = source[name];
      }
    }
  },

  _verifyAbstractImpl: function(constructorClass) {
    var methodsToImplement = new Array;
    var implementedMethods = new Array;
    var superClass = constructorClass;
    while (superClass && superClass.$class) {
      var methods = superClass.$class.getMethods();
      for (var i = 0; i < methods.length; i++) {
        var method = methods[i];
        if (method.isAbstract()) {
          methodsToImplement.push(method);
        } else if (!method.isStatic()) {
          implementedMethods.push(method);
        }
      }
      superClass = superClass.$super;
    }

    var unimplementedMethods = new Array;
    for (var i = 0; i < methodsToImplement.length; i++) {
      var methodFound = false;
      for (var j = 0; j < implementedMethods.length; j++) {
        if (methodsToImplement[i].getName() == implementedMethods[j].getName()
            && constructorClass.prototype[implementedMethods[j].getName()] != js.lang.ClassBuilder.EMPTY_FUNCTION_BODY) {
          methodFound = true;
          break;
        }
      }
      if (!methodFound) {
        unimplementedMethods.push(methodsToImplement[i].getName());
      }
    }

    if (unimplementedMethods.length > 0) {
      throw new js.lang.ClassDefinitionException('Abstract method(s) ' + unimplementedMethods.join(', ') + ' must be implemented');
    }
  },

  _copyFunction: function(f) {
    var fCopy;
    eval("fCopy = " + f.toString() + ";");
    return fCopy;
  },

  _createEmptyFunction: function() {
    return function() {
    };
  }
};

var $import = js.lang.ClassBuilder.createImport;

var $package = js.lang.ClassBuilder.createPackage;

var $extends = js.lang.ClassBuilder.createExtends;

var $class = js.lang.ClassBuilder.createClassDefinition;

var $abstract_class = js.lang.ClassBuilder.createAbstractClassDefinition;